<?php
/**
 * @author jonathan <jo.link.noir@gmail.com>
 * @version 1.4 : jo_link_noir
 * @date 06-07-2010
 */

 /**
 * Classe parent qui comporte les methodes génériques d'une structure (recupère le niveau, vérifie les conditions etc)
 * @exceptions SW_Structure_Exception : voir $errorMessages et getException()
 */
abstract class SW_Structure extends SW_Planet_Storage_Treatment
{
	/** @var string */
	const CLASS_ELEMENT = 'SW_Structure_Element';

	/** @var string */
	const CLASS_EXCEPTION = 'SW_Structure_Exception';


	/** @var array : element débloqué {group_element => group_element, ...} */
	protected $unblock;

	/** @var array : {id_structure => SW_Structure_List, ...} Liste des elements en constructions*/
	protected $lists = array();

	/** @var array : {ref_structure => {id_list => SW_Structure_List, ...}} Liste des elements en constructions*/
	protected $structureLists = array();

	/** @var SW_Structure_DAO_Interface */
	protected $dao;

	/** @var string */
	protected $classElement;

	/** @var string */
	protected $classException;


	/** @var array : contient les messages d'erreur*/
	public $errorMessages = array(
		0 => 'Erreur structure',
		1 => 'L\'élément doit être du type de ',
		2 => 'Structure non terminé',
		3 => 'Structure non débloqué',
		4 => 'Impossible de construire la structure',
		5 => 'Impossible d\'arrêter la construction de la structure',
		6 => 'Impossible de terminé la structure',
		7 => 'Structure déjà en construction',
		8 => 'Structure inexistante',
		9 => 'Liste de la structure inexistante',
		10 => 'Il manque des ressources'
	);


	abstract protected function initElement(SW_Structure_Element $element);


	/**
	 * @param SW_Player $player
	 * @param int $idPlanet
	 * @param SW_Dependency $dependency
	 * @param SW_I_DAO_Structure $dao
	 * @param string $classElement = self::CLASS_ELEMENT : le type de l'élément est vérifier uniquement dans le passage à la méthode add()
	 * @param string $classException = self::CLASS_EXCEPTION
	 */
	public function __construct(SW_Player $player, $idPlanet, SW_Dependency $dependency, SW_Structure_DAO_Interface $dao, $classElement = self::CLASS_ELEMENT, $classException = self::CLASS_EXCEPTION)
	{
		parent::__construct($player, $idPlanet, $dependency);

		$this->dao = $dao;
		$this->classElement = $classElement;
		$this->classException = $classException;

		$this->errorMessages[1] .= $classElement;
	}

	/**
	 * Retourne le texte lié au code d'erreur. Si le code d'erreur n'exsiste pas le message de clef 0 sera retourné.
	 * @param int $code : code du message dans $errorMessages
	 * @return string
	 */
	public function getMessageError($code)
	{
		return $this->errorMessages[isset($this->errorMessages[$code]) ? $code : 0];
	}

	/**
	 * Ajoute un élément
	 * @param SW_Element $element
	 * @exception SW_Structure_Exception : code 1
	 */
	public function add(SW_Element $element)
	{
		if (!($element instanceof $this->classElement))
			throw new $this->classException($this->errorMessages[1], 1);
		parent::add($element);
	}

	/**
	 * Ajoute un élement, l'enregister et retourne un objet de la structure créé
	 * @param int $group
	 * @param int $level = 0
	 * @return SW_Structure_Element
	 */
	public function createElement($group, $level=0)
	{
		$element = $this->dao->createElement($this->idPlanet, $group, $level);
		$this->add($element);
		return $element;
	}

	/**
	 * Ajoute une liste
	 * @param SW_Structure_List $list
	 */
	public function addList(SW_Structure_List $list)
	{
		$this->lists[$list->getId()] = $list;

		if (!isset($this->structureLists[$list->getIdElement()]))
			$this->structureLists[$list->getIdElement()] = array();
		$this->structureLists[$list->getIdElement()][$list->getId()] = $list;
	}

	/**
	 * Supprime une liste
	 * @param int $idList
	 */
	protected function removeList($idList)
	{
		$id_struc = $this->lists[$idList]->getIdElement();
		unset($this->structureLists[$id_struc][$idList], $this->lists[$idList]);
		if (empty($this->structureLists[$id_struc]))
			unset($this->structureLists[$id_struc]);
	}

	/**
	 * @param array $elements : {SW_Structure_Element, ...}
	 * @return int
	 */
	protected function _getLevelMaxGroup(array $elements)
	{
		$lvl = 0;
		foreach ($elements as $element)
		{
			if ($element->getLevel() > $lvl)
				$lvl = $element->getLevel();
		}
		return $lvl;
	}

	/**
	 * @param int $group
	 * @return int
	 */
	public function getLevelMaxGroup($group)
	{
		return isset($this->groupElements[$group])
			? $this->_getLevelMaxGroup($this->groupElements[$group])
			: 0;
	}

	/**
	 * @param int $group
	 * @return array
	 */
	public function getLevelMaxGroups()
	{
		$levelsMax = array();
		foreach ($this->groupElements as $group => $elements)
			$levelsMax[$group] = $this->_getLevelMaxGroup($elements);
		return $levelsMax;
	}

	/**
	 * @param array $elements : {SW_Structure_Element, ...}
	 * @return int
	 */
	protected function _getLevelSumGroup(array $elements)
	{
		$sum  = 0;
		foreach ($elements as $element)
			$sum += $element->getLevel();
		return $sum;
	}

	/**
	 * @param int $group
	 * @return int
	 */
	public function getLevelSumGroup($group)
	{
		return isset($this->groupElements[$group])
			? $this->_getLevelSumGroup($this->groupElements[$group])
			: 0;
	}

	/**
	 * @param int $group
	 * @return array : {group => nombre, ...}
	 */
	public function getLevelSumGroups()
	{
		$levelsSum = array();
		foreach ($this->groupElements as $group => $elements)
			$levelsSum[$group] = $this->_getLevelSumGroup($elements);
		return $levelsSum;
	}

	/**
	 * Initialise les dépendances et initialise les éléments. Retourne l'id des éléments débloqués
	 */
	protected function _init()
	{
		$required = null;
		$this->dependency->affectSolve($this, $this->unblock, $required);

		foreach ($required as $id_group)
		{
			if (!isset($this->groupElements[$id_group]))
				$this->createElement($id_group);
		}

		// initialise le prix et la durée
		foreach ($this->unblock as $id_group)
		{
			if (!isset($this->groupElements[$id_group]))
			{
				if (false !== ($element = $this->createElement($id_group)))
				{
					$this->initElement($element);
				}
			}
			else
			{
				foreach ($this->groupElements[$id_group] as $element)
				{
					$this->initElement($element);
				}
			}
		}
	}

	/**
	 * Accesseur pour unblock
	 * @return ArrayIterator : {id_element => id_element, ...}
	 */
	public function getUnblock()
	{
		return $this->unblock;
	}

	/**
	 * Retourne un tableau de construction d'une structure désigner par $idElement
	 * @param int $id
	 * @return array : {SW_Structure_List, ...}
	 */
	public function getListStructure($id)
	{
		return isset($this->structureLists[$id]) ? $this->structureLists[$id] : array();
	}

	/**
	 * Retourne une liste de construction
	 * @param int $id
	 * @return SW_Structure_List|null
	 */
	public function getList($id)
	{
		return isset($this->lists[$id]) ? $this->lists[$id] : null;
	}

	/**
	 * @return array : {id_list => {SW_Structure_List, ...}}
	 */
	public function getLists()
	{
		return $this->lists;
	}

	/**
	 * Vérifie qu'une structure possède une liste
	 * @param int $id
	 * @return bool
	 */
	public function existListStructure($id)
	{
		return isset($this->structureLists[$id]);
	}

	/**
	 * Vérifie qu'une liste existe
	 * @param int $id
	 * @return bool
	 */
	public function existsList($id)
	{
		return isset($this->lists[$id]);
	}

	/**
	 * Retourne un tableau des groupes SW_Structure_Element débloqué
	 * @return array: {id_group => {id => SW_Structure_Element, ...}, ...}
	 */
	public function getGroupsUnblock()
	{
		$a = array();
		foreach ($this->unblock as $unblock)
			$a[$unblock] = $this->groupElements[$unblock];
		return $a;
	}

	/**
	 * Retourne un tableau des SW_Structure_Element débloqué
	 * @return array: {id => SW_Structure_Element, ...}
	 */
	public function getsUnblock()
	{
		$a = array();
		foreach ($this->unblock as $unblock)
			foreach ($this->groupElements[$unblock] as $id => $e)
				$a[$id] = $e;
		return $a;
	}

	/**
	 * Vérifie si l'élement est débloquer
	 * @param int $group
	 * @return bool
	 */
	public function isUnblock($group)
	{
		return isset($this->unblock[$group]);
	}

	/**
	 * Retourne true lorsque la structure peut construire un élément. Mais ne vérifie pas qu'un élément est constructible (rôle de elementCanBuild).
	 * @param int $timestamp
	 * @return bool|int : si int alors code d'erreur dans $errorMessages (true)
	 */
	public function structureCanBuild($timestamp)
	{
		return true;
	}

	/**
	 * Retourne true si l'élément peut être construit. Mais ne vérifie pas que la structure peut construire (voir structureCanBuild)
	 * @param SW_Structure_Element $element
	 * @param int $timestamp
	 * @return bool|int : si int alors code d'erreur dans $errorMessages (true|3)
	 * @important ne vérifie pas que $element fait partit de la classe
	 */
	public function elementCanBuild(SW_Structure_Element $element, $timestamp)
	{
		return isset($this->unblock[$element->getGroup()]) ? true : 3;
	}

	/**
	 * Retourne true si l'élément peut être construit
	 * @param SW_Structure_Element $element
	 * @param int $timestamp
	 * @return bool|int : si int alors code d'erreur dans $errorMessages
	 */
	final public function canBuild(SW_Structure_Element $element, $timestamp)
	{
		$r = $this->structureCanBuild($timestamp);
		return (true === $r) ? $this->elementCanBuild($element, $timestamp) : $r;
	}

	/**
	 * Commence la construction de la structure demander
	 * @param int $idElement
	 * @param int $timestamp
	 * @param int $count : nombre pour faire évoluer la structure
	 * @return SW_Structure_List
	 * @exception SW_Structure_Exception : code 8 ou 4
	 * @exception SW_Resource_Exception
	 */
	public function build($idElement, $timestamp, $count)
	{
		//inexistant
		if (false === isset($this->elements[$idElement]))
			throw new $this->classException($this->errorMessages[8], 8);

		$element = $this->elements[$idElement];

		//vérifie si la construction peut-être construite
		if (true !== ($can = $this->canBuild($element, $timestamp)))
			throw new $this->classException($this->errorMessages[$can], $can);

		return $this->_build($element, $timestamp, $count);
	}

	/**
	 * lance la construction immédiatement sans faire les vérification de canBuild
	 * @param SW_Structure_Element $element
	 * @param int $timestamp
	 * @param int $count : nombre pour faire évoluer la structure
	 * @return SW_Structure_List
	 * @exception SW_Resource_Exception
	 */
	protected function _build(SW_Structure_Element $element, $timestamp, $count)
	{
		$list = $this->dao->build($this, $element, $timestamp, $count);
		$this->addList($list);
		return $list;
	}

	/**
	 * Retourne true lorsque la structure peut arrêter des éléments. Ne vérifie pas qu'un élément puisse être arrêter (rôle de elementCanDown).
	 * @param int $timestamp
	 * @return bool|int : si int alors code d'erreur dans $errorMessages (true)
	 */
	public function structureCanDown($timestamp)
	{
		return true;
	}

	/**
	 * Retourne true si l'élément peut être arrêter. Mais ne vérifie pas que la structure peut le faire (voir structureCanDown)
	 * @param SW_Structure_List $list
	 * @param int $timestamp
	 * @return bool|int : si int alors code d'erreur dans $errorMessages (true|5) : 5 si la fin de la construction est inférieur à 1s
	 * @important ne vérifie pas que $element fait partit de la classe
	 */
	public function elementCanDown(SW_Structure_List $list, $timestamp)
	{
		return (1 >= $list->getTimeEnd() - $timestamp) ? 5 : true;
	}

	/**
	 * Retourne true si la structure peut être arrêter
	 * @param SW_Structure_List $list
	 * @param int $timestamp
	 * @return bool|int : si int alors code d'erreur dans $errorMessages
	 */
	final public function canDown(SW_Structure_List $list, $timestamp)
	{
		$r = $this->structureCanDown($timestamp);
		return (true === $r) ? $this->elementCanDown($list, $timestamp) : $r;
	}

	/**
	 * Arrête une construction en cours
	 * @param int $idList
	 * @param int $timestamp
	 * @return SW_Structure_List
	 * @exception SW_Structure_Exception : code 9 ou 5
	 */
	public function down($idList, $timestamp)
	{
		$idList = (int)$idList;
		//inexistant
		if (false === isset($this->lists[$idList]))
			throw new $this->classException($this->errorMessages[9], 9);

		$list = $this->lists[$idList];

		if (true !== ($can = $this->canDown($list, $timestamp)))
			throw new $this->classException($this->errorMessages[$can], $can);

		$this->dao->down($this, $list, $timestamp);

		//supprime la liste
		$this->removeList($idList);

		return $list;
	}

	/**
	 * Retourne true lorsque la structure peut finir un élément. Mais ne vérifie pas qu'un élément est fini (rôle de elementCanFinish).
	 * @param int $timestamp
	 * @return bool|int : si int alors code d'erreur dans $errorMessages (true)
	 */
	public function structureCanFinish($timestamp)
	{
		return true;
	}

	/**
	 * Retourne true si l'élément peut être fini. Mais ne vérifie pas que la structure peut le faire (voir structureCanFinish).
	 * @param SW_Structure_List $list
	 * @param int $timestamp
	 * @return bool|int : si int alors code d'erreur dans $errorMessages (true|2)
	 * @important ne vérifie pas que $element fait partit de la classe
	 */
	public function elementCanFinish(SW_Structure_List $list, $timestamp)
	{
		//La construction doit-être terminé
		return ($timestamp < $list->getTimeEnd()) ? 2 : true;
	}

	/**
	 * Retourne true si la structure peut être fini
	 * @param SW_Structure_List $list
	 * @param int $timestamp
	 * @return bool|int : si int alors code d'erreur dans $errorMessages
	 */
	final public function canFinish(SW_Structure_List $list, $timestamp)
	{
		$r = $this->structureCanFinish($timestamp);
		return ($r === true) ? $this->elementCanFinish($list, $timestamp) : $r;
	}

	/**
	 * Fini la construction de la structure demandé
	 * @param SW_Structure_List $list
	 * @param int $timestamp
	 * @exception SW_Structure_Exception
	 */
	protected function _finish(SW_Structure_List $list, $timestamp)
	{
		$element = $this->elements[$list->getIdElement()];
		$element->setTimeEnd($list->getTimeEnd());

		$this->dao->finish($element, $list);

		//supprime la liste si elle ce trouve dans lists
		$this->removeList($list->getId());
	}

	/**
	 * Fini la construction de la structure demandé
	 * @param SW_Structure_List $list
	 * @param int $timestamp
	 * @exception SW_Structure_Exception
	 */
	public function finish(SW_Structure_List $list, $timestamp)
	{
		if (false === isset($this->elements[$list->getIdElement()]) || false === isset($this->lists[$list->getId()]))
			throw new $this->classException($this->errorMessages[8], 8);

		if(($can = $this->canFinish($list, $timestamp)) !== true)
			throw new $this->classException($this->errorMessages[$can], $can);

		$this->_finish($list, $timestamp);
	}

	/**
	 * Fini la construction des structures
	 * @param int $timestamp
	 */
	public function finishCompletedLists($timestamp)
	{
		if (true === $this->structureCanFinish($timestamp))
			foreach ($this->lists as $list)
				if (true === $this->elementCanFinish($list, $timestamp))
					$this->_finish($list, $timestamp);
	}

	/**
	 * Retourne tout les éléments qui peuvent être construit
	 * @param int $timestamp = null : si null alors égal $_SERVER['REQUEST_TIME']
	 * @param null|SW_Resource|string|array|ArrayAccess $resource = null : si string alors utilise getSiblingInstance()
	 * @param array &$codes = null : contient les numéros d'erreur {$id => int, …}
	 * @return array : {id => SW_Structure_Element, ...}
	 * @exception InvalidArgumentException : Parameter 2 ($resource) is not type of SW_Resource, string, array or ArrayAccess
	 * @note si codes existe et que la structure n'accepte pas de construction (structureCanBuild()!==true) alors $codes = {'cannot' => int}
	 */
	final public function getElementsCanBuild($timestamp = null, $resource = null, array &$codes = null)
	{
		if (!$timestamp)
			$timestamp = $_SERVER['REQUEST_TIME'];

		if (true !== $this->structureCanBuild($timestamp))
		{
			if ($codes !== null)
				$codes['cannot'] = $can;
			return array();
		}

		$els = array();
		if ($resource)
		{
			if ($resource instanceof SW_Resource)
				$resources = $resource->getQuantities($timestamp);
			else if (is_string($resources))
				$resources = $this->getSiblingInstance($resource)->getQuantities($timestamp);
			else if (!is_array($resources) && !($resources instanceof ArrayAccess))
				throw new InvalidArgumentException('Parameter 2 ($resource) is not type of SW_Resource, string, array or ArrayAccess');

			foreach ($this->elements as $id => $element)
				if (true === ($can = $this->elementCanBuild($element, $timestamp)))
				{
					foreach ($element->getPrice() as $g => $q)
						if ($resources[$g] < $q)
						{
							$can = false;
							break;
						}

					if ($can)
						$els[$id] = $element;
					else if ($codes !== null)
						$codes[$id] = 10; //ressource insuffisante
				}
				else if ($codes !== null)
					$codes[$id] = $can;
		}
		else
			foreach ($this->elements as $id => $element)
				if (true === ($can = $this->elementCanBuild($element, $timestamp)))
					$els[$id] = $element;
				else if ($codes !== null)
					$codes[$id] = $can;

		return $els;
	}

	/**
	 * Retourne tout les éléments qui peuvent être arrêter
	 * @param int $timestamp = null : si null alors égal $_SERVER['REQUEST_TIME']
	 * @param array &$codes = null : contient les numéro d'erreur {$id => int, …}
	 * @return array : {id => SW_Structure_Element, ...}
	 * @note si codes existe et que la structure n'accepte pas de détruire une construction (structureCanDown()!==true) alors $codes = {'cannot' => int}
	 */
	final public function getElementsCanDown($timestamp = null, array &$codes = null)
	{
		if (!$timestamp)
			$timestamp = $_SERVER['REQUEST_TIME'];

		if (true !== ($can = $this->structureCanDown($timestamp)))
		{
			if ($codes !== null)
				$codes['cannot'] = $can;
			return array();
		}

		$els = array();

		foreach ($this->lists as $id => $element)
			if (true === ($can = $this->elementCanDown($element, $timestamp)))
				$els[$id] = $element;
			else if ($codes !== null)
				$codes[$id] = $can;

		return $els;
	}

	/**
	 * @return SW_I_DAO_Structure
	 */
	public function getDao()
	{
		return $this->dao;
	}
}